在開始之前,
我們先把 Polar AI 匯入專案, 我們過後會使用到裡面的 LLM, TTS, 圖片生成等的功能

AI Dialog Prompt:

實作代碼: AI 對話框功能
1. 使用 Polar AI 的 GeminiCore 進行串接
2. 點擊鍵盤 C 的時候, 會打開 Input Field 的Obj
3. Input Field 輸入完Submit之後, 會關閉 Input Field Obj
4. Input Field 輸入完Submit之後會觸發 GeminiCore, 發送 Chat LLM
5. 回傳的AI資訊要顯示在 DialogBubbleUI 的 Text 上
6. DialogBubbleUI 的Obj過一段時間後會自己隱藏
接下來 Cursor 新生成了以下的檔案:
DialogBubbleUI
這個是用來在北極熊身上顯示對話框的代碼
using UnityEngine;
using UnityEngine.UI;
public class DialogueBubbleUI : MonoBehaviour
{
    [Header("Follow Target")] public GameObject targetObject;
    public float verticalOffset = 0.2f; 
    [Header("Canvas & Transforms")] public Canvas canvas; 
    public RectTransform bubbleRoot; // 氣泡根節點(通常就是掛腳本的 RectTransform)
    public RectTransform backgroundRect; // 背景圖 RectTransform(Image)
    public Text text; // Legacy UnityEngine.UI.Text
    [Header("Sizing")] public Vector2 padding = new Vector2(24f, 16f); // 背景相對文本的內邊距(左右、上下)
    public float maxWidth = 420f; // 最大寬度(超過會換行)
    public float minWidth = 80f; // 最小寬度(背景不會更窄)
    public bool clampToCanvas = true; // 是否把氣泡限制在畫布可見範圍內
    public string contentDemo;
    private Camera worldCam;
    // 佈局狀態守衛,避免重入
    private bool isRefreshingLayout = false;
    private bool deferredScheduled = false;
    // 由佈局引發、用於抵銷高度變化的 Y 偏移(Canvas 本地座標)
    private float layoutYOffset = 0f;
    [Header("Visibility")] public float autoHideSeconds = 4f; // 自動隱藏秒數(<=0 表示不自動隱藏)
    private Coroutine autoHideCoroutine;
    private void Reset()
    {
        bubbleRoot = transform as RectTransform;
        if (!canvas) canvas = GetComponentInParent<Canvas>();
        layoutYOffset = 0f;
    }
    private void Awake()
    {
        if (!canvas) canvas = GetComponentInParent<Canvas>();
        worldCam = ResolveCamera();
    }
    private void OnEnable()
    {
        layoutYOffset = 0f;
        RefreshLayout();
    }
    private void OnDisable()
    {
        deferredScheduled = false;
        isRefreshingLayout = false;
    }
    private Camera ResolveCamera()
    {
        if (!canvas) return Camera.main;
        if (canvas.renderMode == RenderMode.ScreenSpaceOverlay) return null; // Overlay 用 null
        return canvas.worldCamera ? canvas.worldCamera : Camera.main;
    }
    private void LateUpdate()
    {
        UpdateFollow();
    }
    // 移除 TMP 事件回調
    public void SetText()
    {
        if (!text) return;
        SetText(contentDemo);
    }
    public void SetText(string newText)
    {
        if (!text) return;
        text.text = newText ?? string.Empty;
        Canvas.ForceUpdateCanvases();
        RefreshLayout();
        if (!deferredScheduled && isActiveAndEnabled)
        {
            deferredScheduled = true;
            StartCoroutine(DeferredRefreshLayout());
        }
        Show();
        ScheduleAutoHide();
    }
    public void Show()
    {
        if (!gameObject.activeSelf) gameObject.SetActive(true);
    }
    public void Hide()
    {
        if (gameObject.activeSelf) gameObject.SetActive(false);
    }
    public void CancelAutoHide()
    {
        if (autoHideCoroutine != null)
        {
            StopCoroutine(autoHideCoroutine);
            autoHideCoroutine = null;
        }
    }
    public void ScheduleAutoHide()
    {
        CancelAutoHide();
        if (autoHideSeconds > 0f && isActiveAndEnabled)
        {
            autoHideCoroutine = StartCoroutine(AutoHideAfterDelay(autoHideSeconds));
        }
    }
    private System.Collections.IEnumerator AutoHideAfterDelay(float seconds)
    {
        yield return new WaitForSeconds(seconds);
        Hide();
        autoHideCoroutine = null;
    }
    private System.Collections.IEnumerator DeferredRefreshLayout()
    {
        yield return new WaitForEndOfFrame();
        deferredScheduled = false;
        if (!isActiveAndEnabled || !text) yield break;
        Canvas.ForceUpdateCanvases();
        RefreshLayout();
    }
    private void UpdateFollow()
    {
        if (!targetObject || !canvas || !bubbleRoot) return;
        // 以目標的世界座標(含向上偏移)轉成螢幕座標
        Vector3 worldPos = targetObject.transform.position + Vector3.up * verticalOffset;
        Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPos);
        // 佈局造成的 Y 補償
        screenPos.y += layoutYOffset;
        // 設置 UI 位置(直接用螢幕座標)
        bubbleRoot.position = screenPos;
    }
    public void RefreshLayout()
    {
        if (isRefreshingLayout) return;
        if (!text || !backgroundRect || !bubbleRoot) return;
        isRefreshingLayout = true;
        try
        {
            float prevBgH = backgroundRect.rect.height;
            // 確保啟用換行(Legacy Text)
            text.horizontalOverflow = HorizontalWrapMode.Wrap;
            text.verticalOverflow = VerticalWrapMode.Overflow;
            // 計算首選尺寸(Legacy Text)
            float availableMaxTextWidth = Mathf.Max(0f, maxWidth - padding.x * 2f);
            float minTextWidth = Mathf.Max(0f, minWidth - padding.x * 2f);
            // 先計算不受限寬度下的首選寬度
            var settingsNoWrap = text.GetGenerationSettings(Vector2.zero);
            float preferredUnconstrainedWidth = text.cachedTextGeneratorForLayout
                .GetPreferredWidth(text.text, settingsNoWrap) / text.pixelsPerUnit;
            float targetTextWidth = Mathf.Clamp(
                preferredUnconstrainedWidth,
                minTextWidth,
                availableMaxTextWidth > 0 ? availableMaxTextWidth : preferredUnconstrainedWidth
            );
            // 再用限制寬度計算對應高度
            var settingsWithWidth = text.GetGenerationSettings(new Vector2(targetTextWidth, 0f));
            float targetTextHeight = text.cachedTextGeneratorForLayout
                .GetPreferredHeight(text.text, settingsWithWidth) / text.pixelsPerUnit;
            targetTextHeight = Mathf.Max(1f, targetTextHeight);
            // 套用尺寸
            RectTransform textRect = text.rectTransform;
            textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetTextWidth);
            textRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetTextHeight);
            float bgW = targetTextWidth + padding.x * 2f;
            float bgH = targetTextHeight + padding.y * 2f;
            backgroundRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, bgW);
            backgroundRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, bgH);
            // 高度變化補償位置
            float deltaH = bgH - prevBgH;
            if (Mathf.Abs(deltaH) > Mathf.Epsilon)
            {
                float pivotY = bubbleRoot.pivot.y;
                layoutYOffset += deltaH * pivotY;
            }
            // 保守推進一次(避免頻繁重建)
            Canvas.ForceUpdateCanvases();
        }
        finally
        {
            isRefreshingLayout = false;
        }
    }
}
AIChatController
這個是用來控制跟Gemini溝通的功能
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using PolarAI.Scripts.Core.Gemini;
public class AIChatController : MonoBehaviour
{
    [Header("Refs")] public GeminiCore geminiCore;
    public DialogueBubbleUI dialogueBubble;
    [Header("Input UI")] public GameObject inputRoot; // 可整個群組的 Obj
    public InputField inputField; // Legacy UI InputField
    [Header("Settings")] public KeyCode toggleKey = KeyCode.C;
    private bool _showing;
    private void Awake()
    {
        if (inputRoot) inputRoot.SetActive(false);
        _showing = false;
        if (inputField)
        {
            inputField.onEndEdit.AddListener(OnEndEdit);
        }
    }
    private void OnDestroy()
    {
        if (inputField)
        {
            inputField.onEndEdit.RemoveListener(OnEndEdit);
        }
    }
    private void Update()
    {
        if (Input.GetKeyDown(toggleKey))
        {
            ToggleInput();
        }
    }
    private void ToggleInput()
    {
        _showing = !_showing;
        if (inputRoot) inputRoot.SetActive(_showing);
        if (_showing && inputField)
        {
            inputField.text = string.Empty;
            inputField.ActivateInputField();
            inputField.Select();
        }
    }
    private void CloseInput()
    {
        _showing = false;
        if (inputRoot) inputRoot.SetActive(false);
    }
    private void OnEndEdit(string value)
    {
        // 在 Legacy InputField,按 Enter 會觸發 onEndEdit
        // 避免空字串請求
        var userText = (value ?? string.Empty).Trim();
        if (string.IsNullOrEmpty(userText))
        {
            CloseInput();
            return;
        }
        CloseInput();
        SendPrompt(userText);
    }
    private void SendPrompt(string userText)
    {
        if (geminiCore == null)
        {
            Debug.LogWarning("GeminiCore 未綁定");
            if (dialogueBubble) dialogueBubble.SetText("[錯誤] 未設定 GeminiCore");
            return;
        }
        StartCoroutine(CallGemini(userText));
    }
    private IEnumerator CallGemini(string prompt)
    {
        string aiReply = null;
        yield return geminiCore.Chat(
            prompt,
            text => { aiReply = text; }
        );
        if (dialogueBubble)
        {
            dialogueBubble.SetText(aiReply ?? "(無回覆)");
        }
    }
}
基本上這樣代碼就完成了
接下來我們要設定一下 Unity 的場景
在 Dialog 的 UI上掛一個 Dialog Bubble UI, 然後把相關的物件放進去

新增一個 Input Field, 用來輸入 Prompt

新增 GeminiCore 的物件, 掛上 GeminiCore代碼

然後填上你的 Gemini API Key
新增 AIChatController 物件, 把對應的物件拉進去

為了更有沉浸感,
也可以在一開始的時候先提供係統指令
讓 AI 知道你是在跟北極熊寵物對話, 要用什麼語言回復你
( 注意Gemini不支持 system role, 所以可以改成 user )


接下來可以執行遊戲測試
按下 C, 機會打開輸入面板, 然後就可以跟北極熊聊天了